package org.thanos.simulator.api;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.system.SystemUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thanos.bacnet.LocalDevice;
import org.thanos.bacnet.RemoteDevice;
import org.thanos.bacnet.exception.BACnetServiceException;
import org.thanos.bacnet.npdu.ip.IpNetwork;
import org.thanos.bacnet.npdu.ip.IpNetworkBuilder;
import org.thanos.bacnet.obj.AnalogInputObject;
import org.thanos.bacnet.obj.AnalogOutputObject;
import org.thanos.bacnet.obj.AnalogValueObject;
import org.thanos.bacnet.obj.BACnetObject;
import org.thanos.bacnet.obj.BinaryInputObject;
import org.thanos.bacnet.obj.BinaryOutputObject;
import org.thanos.bacnet.obj.BinaryValueObject;
import org.thanos.bacnet.obj.DeviceObject;
import org.thanos.bacnet.obj.MultistateInputObject;
import org.thanos.bacnet.obj.MultistateOutputObject;
import org.thanos.bacnet.obj.MultistateValueObject;
import org.thanos.bacnet.service.unconfirmed.IAmRequest;
import org.thanos.bacnet.service.unconfirmed.WhoIsRequest;
import org.thanos.bacnet.transport.DefaultTransport;
import org.thanos.bacnet.transport.Transport;
import org.thanos.bacnet.type.Encodable;
import org.thanos.bacnet.type.constructed.BACnetArray;
import org.thanos.bacnet.type.enumerated.BinaryPV;
import org.thanos.bacnet.type.enumerated.EngineeringUnits;
import org.thanos.bacnet.type.enumerated.ObjectType;
import org.thanos.bacnet.type.enumerated.Polarity;
import org.thanos.bacnet.type.enumerated.PropertyIdentifier;
import org.thanos.bacnet.type.enumerated.Segmentation;
import org.thanos.bacnet.type.primitive.CharacterString;
import org.thanos.bacnet.type.primitive.Null;
import org.thanos.bacnet.type.primitive.Real;
import org.thanos.bacnet.type.primitive.UnsignedInteger;
import org.thanos.bacnet.util.DiscoveryUtils;
import org.thanos.bacnet.util.RemoteDeviceDiscoverer;
import org.thanos.simulator.page.IndexPage;
import org.thanos.simulator.util.NetworkUtil;
import org.thanos.simulator.util.PropertyUtil;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

public class BACnetIpClient {

    private static final Logger LOGGER = LoggerFactory.getLogger(BACnetIpClient.class);

    private static final List<BACnetObject> RANDOM_OBJECT_LIST = new CopyOnWriteArrayList<>();
    private static LocalDevice localDevice = null;

    public static void init(String ip, int networkNumber, int port, int deviceId, String deviceName, boolean bbmd, int interval) throws Exception {
        if (Objects.nonNull(localDevice) && localDevice.isInitialized()) {
            localDevice.terminate();
        }
        NetworkUtil.NetworkInterfaceDTO dto = NetworkUtil.getNetworkInterfaces().stream().filter(networkInterfaceDTO -> networkInterfaceDTO.getIpAddress().equals(ip)).findAny()
                .orElseThrow(RuntimeException::new);
        IpNetwork network = new IpNetworkBuilder()
                .withLocalNetworkNumber(networkNumber)
                .withLocalBindAddress(SystemUtil.getOsInfo().isWindows() ? ip : IpNetwork.DEFAULT_BIND_IP)
                .withReuseAddress(false)
                .withBroadcast(dto.getBroadcast(), dto.getNetworkPrefixLength())
                .withPort(port).build();
        if (bbmd) {
            network.enableBBMD();
        }
        Transport transport = new DefaultTransport(network);
        localDevice = new LocalDevice(deviceId, transport);
        localDevice.setTimeoutDeviceRetention(0);
        localDevice.writePropertyInternal(PropertyIdentifier.objectName, new CharacterString(deviceName));
        localDevice.writePropertyInternal(PropertyIdentifier.modelName, new CharacterString(deviceName));
        localDevice.writePropertyInternal(PropertyIdentifier.applicationSoftwareVersion, Null.instance);
        localDevice.writePropertyInternal(PropertyIdentifier.firmwareRevision, Null.instance);
        localDevice.writePropertyInternal(PropertyIdentifier.vendorIdentifier, new UnsignedInteger(1));
        localDevice.writePropertyInternal(PropertyIdentifier.vendorName, new CharacterString("ThanosLee"));
        localDevice.writePropertyInternal(PropertyIdentifier.protocolObjectTypesSupported, PropertyUtil.buildObjectTypesSupported());
        localDevice.writePropertyInternal(PropertyIdentifier.protocolServicesSupported, PropertyUtil.buildServicesSupported());
        localDevice.initialize();
        localDevice.sendGlobalBroadcast(new IAmRequest(localDevice.getDeviceObject().getId(), localDevice.get(PropertyIdentifier.maxApduLengthAccepted), Segmentation.segmentedBoth, localDevice.get(PropertyIdentifier.vendorIdentifier)));
//        localDevice.scheduleAtFixedRate(BACnetIpClient::randomUpdateValue, 0, interval, TimeUnit.SECONDS);
    }

    public static void stop() {
        Assert.isTrue(Objects.nonNull(localDevice));
        localDevice.terminate();
    }

    public static boolean isInitialized() {
        return Objects.nonNull(localDevice) && localDevice.isInitialized();
    }

    public static List<RemoteDevice> discover(Integer time, Integer lowLimit, Integer highLimit) {
        List<RemoteDevice> result = null;
        try {
            if (Objects.nonNull(localDevice) && localDevice.isInitialized()) {
                RemoteDeviceDiscoverer deviceDiscoverer = new RemoteDeviceDiscoverer(localDevice);
                if (Objects.nonNull(lowLimit) && Objects.nonNull(highLimit)) {
                    deviceDiscoverer.start(lowLimit, highLimit);
                } else {
                    deviceDiscoverer.start();
                }
                TimeUnit.SECONDS.sleep(time);
                result = deviceDiscoverer.getRemoteDevices();
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        return result;
    }

    public static String getRootDeviceName() {
        return Objects.nonNull(localDevice) ? localDevice.getDeviceObject().getObjectName() : null;
    }

    public static List<BACnetObject> getObjectNames() {
        return Objects.nonNull(localDevice) ? localDevice.getLocalObjects().stream().filter(baCnetObject -> !(baCnetObject instanceof DeviceObject)).collect(Collectors.toList()) : null;
    }

    public static int getNextInstanceNumber(ObjectType objectType) {
        return Objects.nonNull(localDevice) ? localDevice.getNextInstanceObjectNumber(objectType) : 0;
    }

    public static void removeObject(String objectName) throws BACnetServiceException {
        Assert.isTrue(CharSequenceUtil.isNotBlank(objectName));
        Assert.isTrue(Objects.nonNull(localDevice));
        List<BACnetObject> objects = getObjectNames();
        BACnetObject removeObject = null;
        if (CollUtil.isNotEmpty(objects)) {
            removeObject = objects.stream().filter(baCnetObject -> baCnetObject.getObjectName().equals(objectName)).findAny().orElse(null);
        }
        if (Objects.nonNull(removeObject)) {
            RANDOM_OBJECT_LIST.remove(removeObject);
            localDevice.removeObject(removeObject.getId());
        }
    }

    public static void addRandomObject(BACnetObject object) {
        RANDOM_OBJECT_LIST.add(object);
    }

    public static void randomUpdateValue() {
        Assert.isTrue(Objects.nonNull(localDevice));
        if (CollUtil.isEmpty(RANDOM_OBJECT_LIST)) {
            return;
        }
        List<BACnetObject> objects = getObjectNames();
        if (CollUtil.isEmpty(objects)) {
            return;
        }
        for (BACnetObject object : getObjectNames()) {
            if (RANDOM_OBJECT_LIST.contains(object)) {
                Encodable value = null;
                if (PropertyUtil.analogTypes().contains(object.getId().getObjectType())) {
                    value = new Real(RandomUtil.randomBigDecimal().floatValue() * 100);
                } else if (PropertyUtil.binaryTypes().contains(object.getId().getObjectType())) {
                    value = RandomUtil.randomInt(0, 2) == 1 ? BinaryPV.active : BinaryPV.inactive;
                } else if (PropertyUtil.multiStateTypes().contains(object.getId().getObjectType())) {
                    BACnetArray<CharacterString> array = object.get(PropertyIdentifier.stateText);
                    value = new UnsignedInteger(RandomUtil.randomInt(0, array.getCount()));
                }
                object.writePropertyInternal(PropertyIdentifier.presentValue, value);
            }
        }
    }

    public static BACnetObject createAnalogObject(ObjectType objectType, String name, BigDecimal presentValue, Integer unitNumber, Boolean outOfService) throws BACnetServiceException {
        Assert.isTrue(Arrays.asList(ObjectType.analogInput, ObjectType.analogOutput, ObjectType.analogValue).contains(objectType), IllegalArgumentException::new);
        Assert.isTrue(CharSequenceUtil.isNotBlank(name), IllegalArgumentException::new);
        EngineeringUnits unit = Objects.isNull(unitNumber) ? EngineeringUnits.noUnits : EngineeringUnits.forId(unitNumber);
        BACnetObject object = null;
        if (ObjectType.analogInput.equals(objectType)) {
            object = new AnalogInputObject(localDevice, localDevice.getNextInstanceObjectNumber(objectType), name, presentValue.floatValue(), unit, outOfService);
            ((AnalogInputObject) object).supportCovReporting(0.1f);
        } else if (ObjectType.analogOutput.equals(objectType)) {
            object = new AnalogOutputObject(localDevice, localDevice.getNextInstanceObjectNumber(objectType), name, presentValue.floatValue(), unit, outOfService, presentValue.floatValue());
            ((AnalogOutputObject) object).supportCovReporting(0.1f);
        } else if (ObjectType.analogValue.equals(objectType)) {
            object = new AnalogValueObject(localDevice, localDevice.getNextInstanceObjectNumber(objectType), name, presentValue.floatValue(), unit, outOfService);
            ((AnalogValueObject) object).supportCovReporting(0.1f);
        }
        return object;
    }

    public static BACnetObject createBinaryObject(ObjectType objectType, String name, Boolean presentValue, Boolean outOfService) throws BACnetServiceException {
        Assert.isTrue(Arrays.asList(ObjectType.binaryInput, ObjectType.binaryOutput, ObjectType.binaryValue).contains(objectType), IllegalArgumentException::new);
        Assert.isTrue(CharSequenceUtil.isNotBlank(name), IllegalArgumentException::new);
        BACnetObject object = null;
        if (ObjectType.binaryInput.equals(objectType)) {
            object = new BinaryInputObject(localDevice, localDevice.getNextInstanceObjectNumber(objectType), name, BinaryPV.forId(BooleanUtil.toInt(presentValue)), outOfService, Polarity.forId(BooleanUtil.toInt(presentValue)));
            ((BinaryInputObject) object).supportCovReporting();
        } else if (ObjectType.binaryOutput.equals(objectType)) {
            object = new BinaryOutputObject(localDevice, localDevice.getNextInstanceObjectNumber(objectType), name, BinaryPV.forId(BooleanUtil.toInt(presentValue)), outOfService, Polarity.forId(BooleanUtil.toInt(presentValue)), BinaryPV.forId(BooleanUtil.toInt(presentValue)));
            ((BinaryOutputObject) object).supportCovReporting();
        } else if (ObjectType.binaryValue.equals(objectType)) {
            object = new BinaryValueObject(localDevice, localDevice.getNextInstanceObjectNumber(objectType), name, BinaryPV.forId(BooleanUtil.toInt(presentValue)), outOfService);
            ((BinaryValueObject) object).supportCovReporting();
        }
        return object;
    }

    public static BACnetObject createMultiStateObject(ObjectType objectType, String name, List<String> stateTexts, Integer presentValue, Boolean outOfService) throws BACnetServiceException {
        Assert.isTrue(Arrays.asList(ObjectType.multiStateInput, ObjectType.multiStateOutput, ObjectType.multiStateValue).contains(objectType), IllegalArgumentException::new);
        Assert.isTrue(CharSequenceUtil.isNotBlank(name), IllegalArgumentException::new);
        Assert.isTrue(CollUtil.isNotEmpty(stateTexts), IllegalArgumentException::new);
        CharacterString[] states = stateTexts.stream().map(CharacterString::new).collect(Collectors.toList()).toArray(new CharacterString[]{});
        BACnetArray<CharacterString> baCnetArrayStates = new BACnetArray<>(states);
        BACnetObject object = null;
        if (ObjectType.multiStateInput.equals(objectType)) {
            object = new MultistateInputObject(localDevice, localDevice.getNextInstanceObjectNumber(objectType), name, baCnetArrayStates.getCount(), baCnetArrayStates, presentValue, outOfService);
            ((MultistateInputObject) object).supportCovReporting();
        } else if (ObjectType.multiStateOutput.equals(objectType)) {
            object = new MultistateOutputObject(localDevice, localDevice.getNextInstanceObjectNumber(objectType), name, baCnetArrayStates.getCount(), baCnetArrayStates, presentValue, presentValue, outOfService);
            ((MultistateOutputObject) object).supportCovReporting();
        } else if (ObjectType.multiStateValue.equals(objectType)) {
            object = new MultistateValueObject(localDevice, localDevice.getNextInstanceObjectNumber(objectType), name, baCnetArrayStates.getCount(), baCnetArrayStates, presentValue, outOfService);
            ((MultistateValueObject) object).supportCovReporting();
        }
        return object;
    }

}
